package com.changgou.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.changgou.goods.feign.SkuFeign;
import com.changgou.goods.pojo.Sku;
import com.changgou.search.dao.SkuEsMapper;
import com.changgou.search.pojo.SkuInfo;
import com.changgou.search.service.SkuService;
import entity.Result;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.*;

@Service
public class SkuServiceImpl implements SkuService {
    @Autowired(required = false)
    private SkuFeign skuFeign;

    @Autowired
    private SkuEsMapper skuEsMapper;

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;


    @Override
    public void importData() {
        //Feign调用 ，查询List<Sku>
        Result<List<Sku>> skuResult = skuFeign.findAll();

        /***
         * 将Result<List<Sku>>-->[{skuJSON}]-->List<skuInfo>
         *将List<sku> 转成List<skuInfo>
         */
        List<SkuInfo> skuInfoList = JSON.parseArray(JSON.toJSONString(skuResult.getData()), SkuInfo.class);
        /*
        动态规格实现，我们这里发现在筛选的时候，需要提供之前选的规格，而刚才的操作并不能实现
         */
        //循环当前的skuInfoList，
        for (SkuInfo skuInfo : skuInfoList) {
            //获取spec--》map(string)获取的这个map实际是字符串 ->map类型
            //{"电视音响效果":"小电影"， "尺寸":"28"}
            Map map = JSON.parseObject(skuInfo.getSpec(), Map.class);
            //如果需要生成动态的域，只需要将该域存入到一个Map<String,Object>对象中即可，该Map<String,Object>的key会生成一个域，域的名字就是key
            //当前Map<String,Object>的后面的object会作为当前域（key）中的值
            skuInfo.setSpecMap(map);
        }
        //调用dao实现数据批量导入
        skuEsMapper.saveAll(skuInfoList);
    }

    /***
     * 所有的查询操作
     * @param searchMap
     * * key:
     *      *      category     商品分类的过滤查询
     *      *
     *      *      brand        商品品牌的过滤查询   TCL
     *      *
     *      *      spec_规格的名称   value  规格的值      规格相关
     *      *
     *      *      price           value 价格区间的字符串 0-500   3000-*
     *      *
     *      *
     *      *      pageNum   当前的页码
     *      *      pageSize  不用传递   (写死)
     *      *
     *      *
     *      *      sortField  要排序的字段  price
     *      *      sortRule   要排序的规则 (ASC DESC)
     *      *
     *      *
     * @return
     */
    @Override
    public Map search(Map<String, String> searchMap) {
        // 1,创建查询对象 的构建对象
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();

   //=========================过滤查询开始===================================
        // BoolQuery    must,must_not , should
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        //一下是搜索展示给用户的商品
        //2,获取搜索词汇
        if (searchMap != null && searchMap.size() > 0) {
            //根据关键词搜索
            String keywords = searchMap.get("keywords");
            //如果关键字不是空，则根据关键字搜索，增加一个条件
            if (!StringUtils.isEmpty(keywords)) {
                // 3,设置查询条件
                //匹配查询  先分词 再查询  主条件查询
                //参数1 指定要搜索的字段
                //参数2 要搜索的值(先分词 再搜索)

                //输入了关键词
                //nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name",keywords));
                //不分词
                boolQueryBuilder.must(QueryBuilders.queryStringQuery(keywords).field("name"));

            }
            //输入了分类--》category
            if (!StringUtils.isEmpty(searchMap.get("category"))) {
                boolQueryBuilder.must(QueryBuilders.termQuery("categoryName", searchMap.get("category")));
            }
            //输入了品牌-->brand
            if (!StringUtils.isEmpty(searchMap.get("brand"))) {
                boolQueryBuilder.must(QueryBuilders.termQuery("brandName", searchMap.get("brand")));
            }

            //规格过滤实现： 为了区分出来是规格，前台传过来的时候前面加一个标志 如：spec_网络=联通3G&
            for (Map.Entry<String, String> entry : searchMap.entrySet()) {
                String key = entry.getKey();
                //如果key以spec开始，表示规格筛选查询
                // 参数一是 查询的域，我们选不分词哪个域， 第二个是查询的条件
                if (key.startsWith("spec")) {
                    String value = entry.getValue();
                    //spec_网络  spec_要去前五个
                    boolQueryBuilder.must(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", value));
                }
            }

            //将boolQueryBuilder填充给nativeSearchQueryBuilder
            nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
        }

        /*
        价格筛选：
            前台传过来的价格： price： 0-500远  500-1000元  1000-1500元  1500-2000元  20000元以上
            然后我们需要进行处理
         */
        //这里发现获取为空报错，用try包裹处理一下，后续想最好的解决方法
          // 内容：0-500元 或者 20000元以上

            if (!StringUtils.isEmpty(searchMap.get("price"))) {
                String price = searchMap.get("price");

                //我们需要对spice进行去除 元   和 元以上
                price = price.replace("元", "").replace("以上", "");

                //spice根据- 进行分割 然后转为【0-500】或者【2000】这样的数组
                String[] prices = price.split("-");
                // 【x，y】      x一定不为空， 而y可能为空
                if (prices != null && prices.length > 0) {
                    //price>prices[0]
                    //参数1： 搜索的域
                    //参数2： 范围
                    boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.parseInt(prices[0])));
                    if (prices.length == 2) {
                        //price < prices[1]
                        boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.parseInt(prices[1])));
                    }
                }
            }

//==============================过滤查询结束===============================


            //排序实现
            if (!StringUtils.isEmpty(searchMap.get("sortField")) && !StringUtils.isEmpty(searchMap.get("sortRule"))){
                String sortField = searchMap.get("sortField"); //指定排序的域
                String sortRule = searchMap.get("sortRule");  //指定排序的规则
                nativeSearchQueryBuilder.withSort(new FieldSortBuilder(sortField).//指定排序域
                        order(SortOrder.valueOf(sortRule))); //指定排序规则

            }





            /*
           分页搜索实现
            */
            Integer pageNum = 1;
            if (!StringUtils.isEmpty(searchMap.get("pageNum"))) {
                try {
                    pageNum = Integer.parseInt(searchMap.get("pageNum"));
                } catch (NumberFormatException e) {
                    e.printStackTrace();
                    pageNum=1;
                }
            }
            Integer pageSize = 30;
            nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));



        //高亮配置
        HighlightBuilder.Field field = new HighlightBuilder.Field("name");//指定搜索的域

        //增加前缀，进行高亮 <em style ="color:red">
        field.preTags("<em style =\"color:red\">");
        //后缀</em>
        field.postTags("</em>");
        //碎片长度  关键词数据的长度
        field.fragmentSize(100);
        nativeSearchQueryBuilder.withHighlightFields(field);










        //4, 构建查询对象(封装了查询的语法)
        NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();




        // 5,执行查询   因为这个里进行了高亮，所以不能用简单的搜索，需要对结果进行处理，然后在返回
        //AggregatedPage<SkuInfo> skuInfoPage = elasticsearchTemplate.queryForPage(nativeSearchQuery, SkuInfo.class);
        AggregatedPage<SkuInfo> skuInfoPage = elasticsearchTemplate.queryForPage(nativeSearchQuery,  //搜索条件封装
                SkuInfo.class, //数据集合要转化的类型的字节码
                //searchResultMap); //执行搜索后，将数据结果封装到该对象中
        new SearchResultMapper() {
            @Override

            public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
                //储存转化后的对象
                ArrayList<T> skuList = new ArrayList<>();
                //执行查询，获取所有数据-》结果集【非高亮|高亮】  都在这里 searchResponse
                for (SearchHit hit : searchResponse.getHits()) {
                    //分析结果集数据，获取非高亮数据
                    SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(),SkuInfo.class);
                    //分析结果集数据，获取高亮数据-》只有某个域的高亮数据
                    HighlightField highlightField = hit.getHighlightFields().get("name");
                    if (highlightField != null && highlightField.getFragments() != null){
                        //高亮数据读出来
                        Text[] fragments=  highlightField.getFragments();
                        StringBuffer buffer = new StringBuffer();
                        for (Text fragment : fragments) {
                            buffer.append(fragment.toString());
                        }
                        //非高亮数据中指定的域替换成高亮数据
                        skuInfo.setName(buffer.toString());
                    }
                    //将高亮数据添加到集合
                    skuList.add((T) skuInfo);
                }

                /*
                数据返回
                    1）搜索的集合数据： （携带高亮）List<T> content
                    2)分页对象信息：Pageable pageable
                    3）搜索记录的总条数，long total
                 */
                return new AggregatedPageImpl<T>(skuList,pageable,searchResponse.getHits().getTotalHits());
            }
        }
        );



        // 6,返回结果
        List<SkuInfo> skuInfoList = skuInfoPage.getContent();//当前的页的集合 展示所有商品
        int totalPages = skuInfoPage.getTotalPages(); //总页数
        long totalElements = skuInfoPage.getTotalElements();//总记录数

        //封装为map集合返回
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("rows", skuInfoList);
        resultMap.put("total", totalPages);
        resultMap.put("pageNum",pageNum);
        resultMap.put("pageSize",pageSize);
        resultMap.put("totalPages", totalElements);


        //获取搜索封装信息
        Pageable pageable = nativeSearchQuery.getPageable();//分页信息
        int pageNumber = pageable.getPageNumber();//当前页
        int size = pageable.getPageSize();//每页显示的条数
        resultMap.put("pageNumber",pageNumber);
        resultMap.put("size",size);


        //展示给用户商品查询到这里返回结束


      /*  由于我们进行了代码的优化，所以这些之前的代码进行注释，后续可以一步一步的理解
      //当用户选用了分类，将分类作为搜索条件，则不需要对分类进行分组搜索，因为分类分组查询是为了显示让用户有进行筛选的条件
        //分类分组查询
        if (searchMap == null || StringUtils.isEmpty(searchMap.get("category"))) {
            List<String> groupList = getStringsCategoryList(nativeSearchQueryBuilder);
            resultMap.put("categoryList", groupList);
        }

        //当用户选用了品牌，将品牌作为搜索条件，则不需要对品牌进行分组搜索，因为品牌分组查询是为了显示，让用户有进行筛选的条件
        //商品品牌分组查询
        if (searchMap == null || StringUtils.isEmpty(searchMap.get("brand"))) {
            List<String> stringBrandList = getStringBrandList(nativeSearchQueryBuilder);
            resultMap.put("BrandList", stringBrandList);
        }

        //商品规格查询
        Map<String, Set<String>> SpecList = getStringSpecList(nativeSearchQueryBuilder);
        resultMap.put("SpecList", SpecList);*/

        //调这个方法，等同于上面的 分类查询，品牌查询，规格查询

        Map<String, Object> groupMap = searchGroupList(nativeSearchQueryBuilder, searchMap);
        //封装给resultMap进行返回
        resultMap.putAll(groupMap);
        return resultMap;
    }



    /***
     * 分组查询-》分类分组，品牌分组，规格分组
     * @param nativeSearchQueryBuilder
     * @return
     */
    private Map<String,Object> searchGroupList(NativeSearchQueryBuilder nativeSearchQueryBuilder,Map<String, String> searchMap) {
        /***
         * 分组查询分类集合
         * addAggregation:添加一个聚合操作
         * 1) 取别名
         * 2）表示根据哪个域进行分组查询
         *
         */

        // 定义一个map储存所有分组结果
        Map<String,Object> groupMapResult = new HashMap<>();


        //当用户选用了分类，将分类作为搜索条件，则不需要对分类进行分组搜索，因为分类分组查询是为了显示让用户有进行筛选的条件
        //分类分组查询
        if (searchMap == null || StringUtils.isEmpty(searchMap.get("category"))) {
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategory").field("categoryName"));
        }
        //当用户选用了品牌，将品牌作为搜索条件，则不需要对品牌进行分组搜索，因为品牌分组查询是为了显示，让用户有进行筛选的条件
        //商品品牌分组查询
        if (searchMap == null || StringUtils.isEmpty(searchMap.get("brand"))) {
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrand").field("brandName"));
        }
        //规格查询
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpec").field("spec.keyword"));

        AggregatedPage<SkuInfo> aggregatedPage = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class);
        /***
         * 获取分组数据
         * 分组查询会把相同的作为一个，然后把不同的作为一个组，类似于数据库分组查询
         *  aggregatedPage.getAggregations():获取的是集合，可以根据多个域进行分组
         *  .get("skuCategory"): 获取指定的域的集合数， 这里的参数为别名， {手机，家用电器，手机配件}
         */

        //用户选择了分类则不查询  由于现在不是整个方法，所以每个上面都要加条件
        if (searchMap == null || StringUtils.isEmpty(searchMap.get("category"))) {
            StringTerms categoryTerms = aggregatedPage.getAggregations().get("skuCategory");
            //获取分类分组集合数据
            List<String> categoryList = getGourdList(categoryTerms);
            groupMapResult.put("categoryList",categoryList);
        }
        //当用户选用了品牌，将品牌作为搜索条件，则不需要对品牌进行分组搜索，因为品牌分组查询是为了显示，让用户有进行筛选的条件
        //商品品牌分组查询
        if (searchMap == null || StringUtils.isEmpty(searchMap.get("brand"))) {
            StringTerms brandTerms = aggregatedPage.getAggregations().get("skuBrand");
            //获取品牌分组集合数据
            List<String> brandList = getGourdList(brandTerms);
            groupMapResult.put("brandList",brandList);
        }
        //规格查询
        StringTerms specTerms = aggregatedPage.getAggregations().get("skuSpec");
        //获取规格分组集合数据 -->实现合并操作
        List<String> specList = getGourdList(specTerms);
        Map<String, Set<String>> specMap = putAllSpec(specList);
        groupMapResult.put("specMap",specMap);
        return groupMapResult;
    }
    public List<String> getGourdList(StringTerms stringTerms) {
        List<String> groupList = new ArrayList<>();
        for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
            String fileName = bucket.getKeyAsString();//其中一个分类的名字
            groupList.add(fileName);
        }
        return groupList;
    }

    //以上代替一下所有方法，而且查询索引库进行了一次，优化了效率和代码量




    /***
     * 分类分组查询
     * @param nativeSearchQueryBuilder
     * @return
     */
    private List<String> getStringsCategoryList(NativeSearchQueryBuilder nativeSearchQueryBuilder) {
        /***
         * 分组查询分类集合
         * addAggregation:添加一个聚合操作
         * 1) 取别名
         * 2）表示根据哪个域进行分组查询
         *
         */

        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategory").field("categoryName"));
        AggregatedPage<SkuInfo> aggregatedPage = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class);
        /***
         * 获取分组数据
         * 分组查询会把相同的作为一个，然后把不同的作为一个组，类似于数据库分组查询
         *  aggregatedPage.getAggregations():获取的是集合，可以根据多个域进行分组
         *  .get("skuCategory"): 获取指定的域的集合数， 这里的参数为别名， {手机，家用电器，手机配件}
         */
        StringTerms stringTerms = aggregatedPage.getAggregations().get("skuCategory");
        List<String> groupList = new ArrayList<>();
        for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
            String categoryName = bucket.getKeyAsString();//其中一个分类的名字
            groupList.add(categoryName);
        }
        return groupList;
    }

    /***
     * 品牌的分组查询
     * @param nativeSearchQueryBuilder
     * @return
     */
    private List<String> getStringBrandList(NativeSearchQueryBuilder nativeSearchQueryBuilder) {
        /**
         * 聚合查询品牌， 然后进行分组
         * 1）第一个参数是别名
         * 2）第二个参数是查询哪个域
         * {小米，华为，中兴}
         */

        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrand").field("brandName"));
        /***
         * 获取分组数据
         *  aggregatedPage.getAggregations():获取的是集合，可以根据多个域进行分组
         *  .get("skuCategory"): 获取指定的域的集合数， 这里的参数为别名，
         */
        AggregatedPage<SkuInfo> aggregatedPageBrand = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class);
        StringTerms brandTerms = aggregatedPageBrand.getAggregations().get("skuBrand");
        List<String> brandList = new ArrayList<>();
        for (StringTerms.Bucket bucket : brandTerms.getBuckets()) {
            String brandName = bucket.getKeyAsString(); //得到每个品牌的名字
            brandList.add(brandName);
        }
        return brandList;
    }

    /**
     * 搜索规格查询
     *
     * @param nativeSearchQueryBuilder
     */
    private Map<String, Set<String>> getStringSpecList(NativeSearchQueryBuilder nativeSearchQueryBuilder) {
        /**
         * 聚合查询规格， 然后进行分组
         * 1）第一个参数是别名
         * 2）第二个参数是查询哪个域
         * 3) 展现多少个规格
         */
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpec").field("spec.keyword").size(10000));
        /***
         * 获取分组数据
         *  aggregatedPage.getAggregations():获取的是集合，可以根据多个域进行分组
         *  .get("skuCategory"): 获取指定的域的集合数， 这里的参数为别名，
         *  {"电视音响效果":"环绕","电视屏幕尺寸":"60英寸","尺码":"175"}
         *  {"电视音响效果":"立体声","电视屏幕尺寸":"60英寸","尺码":"170"}
         */
        AggregatedPage<SkuInfo> aggregatedPageBrand = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class);
        StringTerms skuSpecTerms = aggregatedPageBrand.getAggregations().get("skuSpec");
        List<String> specList = new ArrayList<>();
        for (StringTerms.Bucket bucket : skuSpecTerms.getBuckets()) {
            String specName = bucket.getKeyAsString();
            specList.add(specName);
        }

        return putAllSpec(specList);
    }

    // 把
    private Map<String, Set<String>> putAllSpec(List<String> specList) {
        // 合并后的map 对象
        //3,将每个map对象合成一个map<String,Set<String>>
        Map<String, Set<String>> allSpec = new HashMap<>();

        // 1,循环遍历specList  { "电视音响效果":"立体声","电视屏幕尺寸":"20英寸","尺码":"165"}
        for (String spec : specList) {
            // 2, 将每个JSON字符串转成map
            Map<String, String> specMap = JSON.parseObject(spec, Map.class);


            //4,合并流程
            // 4,1 循环所有map
            for (Map.Entry<String, String> entry : specMap.entrySet()) {

                // 4,2 取出当前map ，并且获取对应的key，以及value
                String key = entry.getKey(); //规格名字
                String value = entry.getValue(); //规格值
                Set<String> specSet = allSpec.get(key); //通过获取的key，去查找当前map集合中是否有对应的值
                //这里刚开始的方案，会造成每次new set 然后map中的set集合里总是只储存一个值
                //如果当前的值为空，说明是第一次储存该键值，则新建一个set集合 实现 { "电视音响效果":"立体声","单声道","双声道"}
                if (specSet == null) {
                    specSet = new HashSet<>();

                }
                specSet.add(value);

                // 4,3 将当前循环的数据合并到一个map<String,Set<String>>中 ， 如果key存在，则会更新key对应值
                allSpec.put(key, specSet);
            }

        }
        return allSpec;
    }

}
